import wx

import ab_ids as ai
import ab_cache
import ab_plugins
import ab_utils

import ab_config as ac

from ab_field import AtrField
from ab_statusbar import StatusBar


class TextViewerCtrl(wx.ScrolledWindow):
    def __init__(self, parent, app, opaque):
        wx.ScrolledWindow.__init__(self, parent, -1,
                                   style=wx.EXPAND | wx.SUNKEN_BORDER)
        self.EnableScrolling(True, True)
        # set the windows color
        self.SetBackgroundColour(
            wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW))

        # initialize the parameters
        self.of = 0
        self.pt = 0

        self.sx = 0L
        self.sy = 0L
        self.cof = -1

        self.selections = list()

        self.app = app
        self.parent = parent
        self.attribute = 0

        self.refresh = False
        self.kepfocus = False

        self.opaque = opaque.getFile()
        self.plx_name = opaque.getName()

        self.app.debug('<TextViewerCtrl.__init__> Enter')
        # Global objects
        self.plx = self.app.readKey(ai.CfgParaPlxObject)

        # remember cursor offset
        self.cur_pos = 0
        self.lst = [0, 0]
        self.led = [0, 0]

        self.nbline = self.app.readKey(ai.CfgParaLineLength)
        self.caches = ab_cache.LineCache(self.opaque, self.nbline,
                                         opaque.getAccelerator())

        self.schar = self.caches.getMaxLength()

        # update the display
        self.tpinfo = 0
        self.dlginfo = None

        # set the popup Menu
        self.popmenu = self.buildPopupMenu()

        # set focus
        self.Bind(wx.EVT_SET_FOCUS, self.OnSetFocus)
        self.Bind(wx.EVT_KILL_FOCUS, self.OnKillFocus)
        # bind the right click
        self.Bind(wx.EVT_PAINT, self.OnPaint)
        self.Bind(wx.EVT_SIZE, self.OnSize)
        self.Bind(wx.EVT_SCROLLWIN, self.OnScroll)
        self.Bind(wx.EVT_RIGHT_UP, self.OnRightUp)

        self.Bind(wx.EVT_LEFT_DOWN, self.OnMouseMotion)
        self.Bind(wx.EVT_LEFT_UP, self.OnMouseMotion)

        self.setFont()
        self.OnSize(None)

        self.app.debug('<TextViewerCtrl.__init__> Exit')

    def buildPopupMenu(self):
        menu = wx.Menu()

        menu.Append(ai.id_Save_Binary, 'Save as &Binary')
        menu.Append(ai.id_Save_Hex, 'Save as &Hexademical')
        menu.AppendSeparator()
        menu.Append(ai.id_Goto, '&Goto Position...')
        menu.Append(wx.ID_COPY, '&Copy Selected')

        idmgr = self.app.getWindow().getIdsManager()
        # generators
        gen_list = self.plx.getPlxList(ab_plugins.PlxTpGenerator)
        gen_list = self.plx.getFilteredPlxList(gen_list, ab_plugins.PlxCtxTree)
        if len(self.caches) == 0 and len(gen_list) > 0:
            submenu = wx.Menu()
            for plx_name in gen_list:
                submenu.Append(idmgr.getExportedIdWithPlx(plx_name), plx_name)

            menu.AppendSeparator()
            menu.AppendMenu(-1, 'Exported With', submenu)

        exports = self.plx.getCreators()
        if len(exports) > 0:
            submenu = wx.Menu()
            for name, export_list in exports:
                if len(export_list) == 1:
                    submenu.Append(
                        idmgr.getCreatorIdWithPlx(name, export_list[0]),
                        '%s:%s' % (self.plx.getElement(name).getShortname(),
                                   export_list[0]))
                elif len(export_list) > 0:
                    export_menu = wx.Menu()
                    for e in export_list:
                        export_menu.Append(
                            idmgr.getCreatorIdWithPlx(name, e), e)

                    submenu.AppendMenu(idmgr.getCreatorIdWithPlx(name),
                                       name, export_menu)

            menu.AppendSeparator()
            menu.AppendMenu(-1, 'Created With', submenu)

        return menu

    def OnScroll(self, event):
        self.app.debug('<TextViewerCtrl.OnScroll> Enter')
        et = event.GetEventType()

        (x, y) = (self.sx, self.sy)
        on = [0, 1][event.GetOrientation() == wx.HORIZONTAL]
        va = [self.sy, self.sx][on]

        if et == wx.wxEVT_SCROLLWIN_LINEUP:
            va -= 1
        elif et == wx.wxEVT_SCROLLWIN_LINEDOWN:
            va += 1
        elif et == wx.wxEVT_SCROLLWIN_PAGEUP:
            va -= [self.sh, self.sw][on]
        elif et == wx.wxEVT_SCROLLWIN_PAGEDOWN:
            va += [self.sh, self.sw][on]
        elif et == wx.wxEVT_SCROLLWIN_TOP:
            va = 0
        elif et == wx.wxEVT_SCROLLWIN_BOTTOM:
            va = self.lines - self.sh
        else:
            va = event.GetPosition()

        if on:
            self.sx = max(0, min(va, self.schar - self.sw))
        else:
            self.sy = max(0, min(va, self.lines - self.sh))

        if (x, y) != (self.sx, self.sy):
            self.update()

        self.app.debug('<TextViewerCtrl.OnScroll> Exit')

    def OnPaint(self, event):
        self.app.debug('<TextViewerCtrl.OnPaint> Enter')
        dc = wx.PaintDC(self)
        dc.DrawBitmap(self.bmp, 0, 0, False)
        self.app.debug('<TextViewerCtrl.OnPaint> Exit')

    def OnRightUp(self, event):
        self.PopupMenu(self.popmenu)

    def OnMouseMotion(self, event):
        up = False
        linesel = False
        coff = -1
        self.SetFocus()
        x, y = (event.GetX(), event.GetY())

        ln = y / self.fh + self.sy

        if len(self.caches) > 0:
            xn = x / self.fw
            if 0 <= ln < len(self.caches) - 1:
                coff = self.caches[ln]
                if xn >= 0:
                    coff = self.caches[ln] + \
                        min(xn, self.caches[ln + 1] - self.caches[ln])

            elif ln > 0:
                coff = self.caches[-1]
        else:
            xn = self.caches.getCharOffset(x / self.fw)

            if xn >= 0:
                coff = xn + ln * self.nbline
            else:
                coff = (ln + 1) * self.nbline
                linesel = True

        if event.LeftDown():
            self.CaptureMouse()
            self.OnSetFocus(None)
            self.cof = min(coff, len(self.opaque))
        elif event.Dragging() and self.cof != -1:
            pass
        elif event.LeftUp():
            up = True
            if self.cof != -1:
                self.ReleaseMouse()
            self.cof = -1

        if self.cof != -1 and coff != -1:
            st = min(self.cof, coff)
            lx = abs(coff - self.cof)
            if linesel:
                lx += self.nbline
            elif not lx:
                lx = 1

            if 0 <= st and st + lx <= len(self.opaque):
                self.setOffset(st, lx)

        # tries to highlight the section in the view
        if up and self.app.readKey(ai.CfgParaTreeMatchEnable):
            # handle only with one character selected not to confuse the user
            if len(self.selections) > 0 and self.selections[0].getSize() == 1:
                self.kepfocus = True
                if hasattr(self.parent, 'setSelection'):
                    self.parent.setSelection(self.selections, self)

    def OnSetFocus(self, event):
        self.app.debug('<TextViewerCtrl.OnSetFocus> Enter')

        self.Bind(wx.EVT_MOTION, self.OnMouseMotion)
        if hasattr(self.parent, 'updateFocus'):
            self.parent.updateFocus(self)
        self.app.debug('<TextViewerCtrl.OnSetFocus> Exit')

    def OnKillFocus(self, event):
        self.app.debug('<TextViewerCtrl.OnKillFocus> Enter')

        self.Bind(wx.EVT_MOTION, None)
        self.app.debug('<TextViewerCtrl.OnKillFocus> Exit')

    def OnSize(self, event):
        self.app.debug('<TextViewerCtrl.OnSize> Enter')
        self.bw, self.bh = self.GetClientSizeTuple()
        self.bmp = wx.EmptyBitmap(max(1, self.bw), max(1, self.bh))

        self.updateLineMode()
        self.setCharDimensions()

        if self.sx + self.sw >= self.schar - 1:
            self.sx = max(0, self.schar - self.sw)

        self.update()

        if event is not None:
            event.Skip()

        self.app.debug('<TextViewerCtrl.OnSize> Exit')

    def calculateScollBar(self):
        self.sy = max(0, min(self.sy, self.lines - self.sh))
        if self.lines < 2 ** 25:
            self.SetScrollbars(self.fw, self.fh, self.schar,
                               self.lines, self.sx, self.sy)
        else:
            self.SetScrollbars(self.fw, self.fh, self.sw, 0, self.sx, 0)

    def updateLineMode(self):
        if len(self.caches) > 0:
            self.lines = len(self.caches) - 1
        else:
            if self.app.readKey(ai.CfgParaEnableAutoLine):
                self.setCharDimensions()
                # don't be less than 4
                self.nbline = max(4, (self.sw - self.caches.getSp()[0]) / 4)
            else:
                self.nbline = self.app.readKey(ai.CfgParaLineLength)

            self.lines = len(self.opaque) / self.nbline + 1
            self.caches.reset(self.nbline)

    def setFont(self):
        dc = wx.ClientDC(self)
        dc.SetFont(self.app.readKey(ai.CfgParaDisplayFont))

        self.fw = dc.GetCharWidth()
        self.fh = dc.GetCharHeight()

    def setCharDimensions(self):
        self.sh = self.bh / self.fh
        self.sw = (self.bw / self.fw) - 1

    def setSelection(self, section):
        self.selections = list(section)
        self.refresh = True

        self.update()
        if not isinstance(self.parent, wx.MDIChildFrame) and \
                hasattr(self.parent, 'updateStatus') and \
                len(section or list()) > 0:
            self.parent.updateStatus(StatusBar.INFO_SELECTED, state=section[0])

    def setOffset(self, offset, length):
        self.setSelection((AtrField(offset, length),))

        return offset

    def update(self, dc=None):
        if dc is None:
            cdc = wx.ClientDC(self)
            dc = wx.BufferedDC(cdc, self.bmp)

        if dc.Ok():
            self.draw(dc)
            self.calculateScollBar()

    def drawh(self, dc, sect, color):
        a, b = sect.getZone()
        # draw a line more, self.sx and self.sy is correct
        if len(self.caches) > 0:
            dc.SetBrush(wx.Brush(color))
            ay = 0
            # find the first line
            while ay + 1 < len(self.caches):
                if self.caches[ay + 1] >= a:
                    break
                ay += 1

            while b > 0 and ay + 1 < len(self.caches):
                by = ay - self.sy
                a1 = max(0, a - self.caches[ay])
                a2 = min(b, self.caches[ay + 1] - self.caches[ay] - a1)

                b -= self.caches[ay + 1] - self.caches[ay] - a1
                # adjust the X offset
                if a1 < self.sx:
                    a2 -= self.sx - a1
                    a1 = 0
                else:
                    a1 -= self.sx
                if a2 > 0:
                    dc.DrawRectangle(self.fw * a1, self.fh * by,
                                     self.fw * a2, self.fh)

                ay += 1
        else:
            # find out displable section
            sa = self.nbline * self.sy
            sb = self.nbline * self.sh

            if a >= sa + sb + self.nbline or a + b + self.nbline <= sa:
                return

            ay, ax = divmod(a, self.nbline)
            by, bx = divmod(a + b, self.nbline)

            # adjust the end of sectiion
            if bx == 0:
                by -= 1
                bx = self.nbline

            dc.SetBrush(wx.Brush(color))

            ay -= self.sy
            by -= self.sy

            # draw first line
            if ax and ay >= 0:
                # only a line is selected
                if ay == by:
                    w = bx - ax
                else:
                    w = self.nbline - ax

                r1, a1, a2, r2, b1, b2 = self.caches.getOffset(ax, w, self.sx)
                if r1:
                    dc.DrawRectangle(self.fw * a1, self.fh * ay,
                                     self.fw * a2, self.fh)
                if r2:
                    dc.DrawRectangle(self.fw * b1, self.fh * ay,
                                     self.fw * b2, self.fh)
                ay += 1

            # draw last line
            if bx and ay <= by and by < self.sh + 1:
                r1, a1, a2, r2, b1, b2 = self.caches.getOffset(0, bx, self.sx)
                if r1:
                    dc.DrawRectangle(self.fw * a1, self.fh * by,
                                     self.fw * a2, self.fh)
                if r2:
                    dc.DrawRectangle(self.fw * b1, self.fh * by,
                                     self.fw * b2, self.fh)

            for x in range(max(0, ay), min(by, self.sh + 1)):
                r1, a1, a2, r2, b1, b2, = self.caches.getOffset(0, self.nbline,
                                                                self.sx)
                if r1:
                    dc.DrawRectangle(self.fw * a1, self.fh * x,
                                     self.fw * a2, self.fh)
                if r2:
                    dc.DrawRectangle(self.fw * b1, self.fh * x,
                                     self.fw * b2, self.fh)

    def drawf(self, dc, sect, newcolor, oldcolor):
        a, b = sect.getZone()

        if len(self.caches) > 0:
            dc.SetTextForeground(newcolor)
            ay = 0
            while ay + 1 < len(self.caches):
                if self.caches[ay + 1] >= a:
                    break
                ay += 1

            while b > 0 and ay + 1 < len(self.caches):
                by = ay - self.sy
                a1 = max(0, a - self.caches[ay])
                a2 = min(b, self.caches[ay + 1] - self.caches[ay] - a1)
                b -= self.caches[ay + 1] - self.caches[ay] - a1

                # adjust the X offset
                if a1 + a2 > self.sx:
                    if a1 < self.sx:
                        a2 -= self.sx - a1
                        a1 = 0
                    else:
                        a1 -= self.sx
                    lx = self.caches.getContentWithLine(ay)
                    dc.DrawText(lx[a1 + self.sx:a1 + self.sx + a2],
                                self.fw * a1, self.fh * by)

                ay += 1

            dc.SetTextForeground(oldcolor)
        else:
            # find out displable section
            sa = self.nbline * self.sy
            sb = self.nbline * self.sh

            # draw a line more
            if a >= sa + sb + self.nbline or a + b + self.nbline <= sa:
                return

            ay, ax = divmod(a, self.nbline)
            by, bx = divmod(a + b, self.nbline)
            # adjust the end of sectiion
            if bx == 0:
                by -= 1
                bx = self.nbline

            dc.SetTextForeground(newcolor)

            ay -= self.sy
            by -= self.sy
            # draw first line
            if ax and ay >= 0:
                # only a line is selected
                if ay == by:
                    w = bx - ax
                else:
                    w = self.nbline - ax
                lx = self.caches.getContentWithLine(ay + self.sy)
                r1, a1, a2, r2, b1, b2 = self.caches.getOffset(ax, w, self.sx)
                if r1:
                    dc.DrawText(lx[a1 + self.sx:a1 + self.sx + a2],
                                self.fw * a1, self.fh * ay)
                if r2:
                    dc.DrawText(lx[b1 + self.sx:b1 + self.sx + b2],
                                self.fw * b1, self.fh * ay)
                ay += 1

            if bx and ay <= by and by < self.sh + 1:
                lx = self.caches.getContentWithLine(by + self.sy)
                r1, a1, a2, r2, b1, b2 = self.caches.getOffset(0, bx, self.sx)
                if r1:
                    dc.DrawText(lx[a1 + self.sx:a1 + self.sx + a2],
                                self.fw * a1, self.fh * by)
                if r2:
                    dc.DrawText(lx[b1 + self.sx:b1 + self.sx + b2],
                                self.fw * b1, self.fh * by)

            for x in range(max(0, ay), min(by, self.sh + 1)):
                lx = self.caches.getContentWithLine(x + self.sy)
                r1, a1, a2, r2, b1, b2, = self.caches.getOffset(0, self.nbline,
                                                                self.sx)
                if r1:
                    dc.DrawText(lx[a1 + self.sx:a1 + self.sx + a2],
                                self.fw * a1, self.fh * x)
                if r2:
                    dc.DrawText(lx[b1 + self.sx:b1 + self.sx + b2],
                                self.fw * b1, self.fh * x)

            dc.SetTextForeground(oldcolor)

    def draw(self, dc):
        # initialize the DC
        dc.BeginDrawing()
        dc.Clear()

        dc.SetFont(self.app.readKey(ai.CfgParaDisplayFont))
        self.setCharDimensions()

        font_color = self.app.readKey(ai.CfgParaColorFontHigh)
        back_color = self.app.readKey(ai.CfgParaColorBackHigh)
        line_color = self.app.readKey(ai.CfgParaColorSeparator)

        # draw the highlight fields, attention the order as the priorities
        for f in self.selections:
            # re-locate the start pointer
            if self.refresh:
                if len(self.caches) > 0:
                    y = max(0,
                            self.caches.getLineNumWithOffset(f.getStart()) - 1)
                else:
                    y = f.getStart() / self.nbline

                if not self.sy <= y <= self.sy + self.sh:
                    self.sy = y
                    self.refresh = False
                    if self.sy > max(0, self.lines - self.sh):
                        self.sy = max(0, self.lines - self.sh)

            self.drawh(dc, f, ab_utils.int2rgb(back_color))

        # display the context
        y = 0
        for li in range(self.sy, min(self.sy + self.sh + 1, self.lines)):
            lx = self.caches.getContentWithLine(li)[self.sx:]
            # draw the text
            if len(lx) > 0:
                dc.DrawText(lx, 0, y)
            y += self.fh

        # draw the text again with foreground
        for s in self.selections:
            self.drawf(dc, s, ab_utils.int2rgb(font_color),
                       dc.GetTextForeground())

        # draw the column lines
        if len(self.caches) == 0:
            sa, sb = self.caches.getSp()

            dc.SetPen(wx.Pen(wx.Colour(*ab_utils.int2rgb(line_color)), 1))
            x = (sa - self.sx + 0.5) * self.fw
            if x > 0:
                dc.DrawLine(x, 0, x, self.bh)
            x = (sb - self.sx + 0.5) * self.fw
            if x > 0:
                dc.DrawLine(x, 0, x, self.bh)

        dc.EndDrawing()

    def getSelection(self):
        return self.selections

    def execFind(self, data, flags):
        backward = (flags & wx.FR_DOWN) != wx.FR_DOWN
        # don't support case sensitive
        if self.cur_pos == 0:
            position = 0
        elif backward:
            position = self.cur_pos - 1
        else:
            position = self.cur_pos + 1

        offset = self.opaque.find(data, position, backward)
        if offset != -1:
            self.cur_pos = self.setOffset(offset, len(data))
            return True

        return False
